DLO-JZ Profiler, DataLoader, WebDataset, Data Augmentation - Jour 2¶

car

Objet du notebook¶

Le but de ce notebook est d'optimiser la DataLoader afin de ne pas ralentir la boucle d'apprentissage. L'étude de la performance des solutions optimisées se fera en visualisant les traces du profiler :

  • TP 0 : Profiler
  • TP 1 : Optimisation du DataLoader
  • TP 2 : Optimisation du DataLoader au format Webdataset
  • TP 3 : Data Augmentation

Les cellules dans ce notebook ne sont pas prévues pour être modifiées, sauf rares exceptions indiquées dans les commentaires. Les TP se feront en modifiant le code dlojz.py.

Les directives de modification seront marquées par l'étiquette TODO : dans le notebook suivant.

Les solutions sont présentes dans le répertoire solutions.

Notebook rédigé par l'équipe assistance IA de l'IDRIS, février 2023


Environnement de calcul¶

Un module PyTorch doit avoir été chargé pour le bon fonctionnement de ce Notebook. Nécessairement, le module pytorch-gpu/py3/1.11.0 :

In [1]:
!module list
Currently Loaded Modulefiles:
 1) cuda/11.2             5) openmpi/4.1.1-cuda   9) sparsehash/2.0.3        
 2) nccl/2.9.6-1-cuda     6) intel-mkl/2020.4    10) libjpeg-turbo/2.1.3     
 3) cudnn/8.1.1.33-cuda   7) magma/2.5.4-cuda    11) pytorch-gpu/py3/1.11.0  
 4) gcc/8.4.1(8.3.1)      8) sox/14.4.2          
>

Les fonctions python de gestion de queue SLURM dévelopées par l'IDRIS et les fonctions dédiées à la formation DLO-JZ sont à importer.

Le module d'environnement pour les jobs et la taille des images sont fixés pour ce notebook.

TODO : choisir un pseudonyme (maximum 5 caractères) pour vous différencier dans la queue SLURM et dans les outils collaboratifs pendant la formation et la compétition.

In [2]:
from idr_pytools import display_slurm_queue, gpu_jobs_submitter, search_log
from dlojz_tools import controle_technique, compare, GPU_underthehood, plot_accuracy, lrfind_plot, imagenet_starter, turbo_profiler
MODULE = 'pytorch-gpu/py3/1.11.0'
account = 'for@v100'
name = 'pseudo'   ## Pseudonyme à choisir

Gestion de la queue SLURM¶

Cette partie permet d'afficher et de gérer la queue SLURM.

Pour afficher toute la queue utilisateur :

In [3]:
display_slurm_queue(name)
 Done!

Remarque: Cette fonction utilisée plusieurs fois dans ce notebook permet d'afficher la queue de manière dynamique, rafraichie toutes les 5 secondes. Cependant elle ne s'arrête que lorsque la queue est vide. Si vous désirez reprendre la main sur le notebook, il vous suffira d'arrêter manuellement la cellule avec le bouton stop. Cela a bien sûr aucun impact sur le scheduler SLURM. Les jobs ne seront pas arrêtés.

Si vous voulez annuler un job dans votre queue, décommenter la ligne suivante et remplacer le numéro du job.

In [4]:
#!scancel 2088207

Debug¶

Cette partie debug permet d'afficher les fichiers de sortie et les fichiers d'erreur du job.

Il est nécessaire dans la cellule suivante d'indiquer le jobid correspondant sous le format donné.

*Remarque* : dans ce notebook, lorsque vous soumettrez un job, vous recevrez en retour le numéro du job dans le format suivant : jobid = ['123456']. La cellule ci-dessous peut ainsi être facilement actualisée.

In [5]:
#jobid = ['2088207']

Fichier de sortie :

In [6]:
%cat {search_log(contains=jobid[0])[0]}
/bin/bash: -c: line 0: syntax error near unexpected token `('
/bin/bash: -c: line 0: `cat {search_log(contains=jobid[0])[0]}'

Fichier d'erreur :

In [7]:
%cat {search_log(contains=jobid[0], with_err=True)['stderr'][0]}
/bin/bash: -c: line 0: syntax error near unexpected token `('
/bin/bash: -c: line 0: `cat {search_log(contains=jobid[0], with_err=True)['stderr'][0]}'

Différence entre deux scripts¶

Pour le debug ou pour comparer son code avec les solutions mises à disposition, la fonction suivante permet d'afficher une page html contenant un différentiel de fichiers texte.

In [98]:
s1 = "./solutions/dlojz2_3.py"
s2 = "./solutions/dlojz2_4.py"
compare(s1, s2)

Voir le résultat du différentiel de fichiers sur la page suivante (attention au spoil !) :

compare.html


TP2_0 : Profiler¶

On fixe le batch size et la taille d'image pour ce TP.

In [13]:
bs_optim = 512
image_size = 176

TODO : Comparer votre script dlojz.py avec ce qu'il devrait être actuellement. Si il y a des divergences, veuillez les corriger (par exemple en copiant-collant la solution).

In [ ]:
s1 = "dlojz.py"
s2 = "./solutions/dlojz1_4.py"
compare(s1, s2)

Voir le résultat du différentiel de fichiers sur la page suivante :

compare.html

In [12]:
# copier/coller la solution si nécessaire
!cp solutions/dlojz1_4.py dlojz.py

Implémentation du profiler PyTorch¶

TODO : implémenter le profiler natif PyTorch dans le script dlojz.py :

  • Importer les fonctionnalités liées au Profiler Pytorch.
from torch.profiler import profile, tensorboard_trace_handler, ProfilerActivity, schedule
  • Dans la définition des arguments du script, ajouter l'option --prof.
parser.add_argument('--prof', default=False, action='store_true', help='PROF implementation')
  • Avant la boucle d'apprentissage, définir un context prof avec les paramètres du profiler. contextlib.nullcontext() définit un context null lorsque l'on utilise pas le Profiler.
# Pytorch profiler setup
    prof =  profile(activities=[ProfilerActivity.CPU, ProfilerActivity.CUDA],
                    schedule=schedule(wait=1, warmup=1, active=12, repeat=1),
                    on_trace_ready=tensorboard_trace_handler('./profiler/' + os.environ['SLURM_JOB_NAME'] 
                                               + '_' + os.environ['SLURM_JOBID'] + '_bs' +
                                               str(mini_batch_size)  + '_is' + str(args.image_size)),
                    profile_memory=True,
                    record_shapes=False, 
                    with_stack=False,
                    with_flops=False
                    ) if args.prof else contextlib.nullcontext()
  • Puis englober toute la boucle d'apprentissage (validation comprise) dans le context prof.
#### TRAINING ############
    with prof:
        for epoch in range(args.epochs):
            ...
  • Indiquer au profiler la fin de chaque itération d'apprentissage (avant la validation).
# profiler update
    if args.prof: prof.step()

Génération d'une trace profiler¶

Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.

Remarques :

  • le profilage étant effectué sur une douzaine de steps, nous n'exécutons l'entraînement que sur 15 steps grâce à l'argument --test-nsteps=15
  • les arguments --num-workers 0 --no-persistent-workers --no-pin-memory --no-non-blocking --prefetch-factor 2 utilisés dans la commande ci-dessous servent à supprimer certaines optimisations déjà présentes dans le script dlojz.py. Ces optimisations seront détaillées dans le prochain chapitre du cours.

Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.

In [13]:
command = f'dlojz.py -b {bs_optim} --image-size {image_size} --test --test-nsteps 15 --prof'
command += f' --num-workers 0 --no-persistent-workers --no-pin-memory --no-non-blocking --prefetch-factor 2'
n_gpu = 1
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
                    account=account, time_max='00:10:00', constraint='v100-32g')
print(f'jobid = {jobid}')
batch job 0: 1 GPUs distributed on 1 nodes with 1 tasks / 1 gpus per node and 10 cpus per task
Submitted batch job 1731630
jobid = ['1731630']

Puis, rebasculer la cellule précédente en mode Raw NBConvert, afin d'eviter de relancer un job par erreur.

In [14]:
display_slurm_queue(name)
             JOBID PARTITION     NAME     USER ST       TIME  NODES NODELIST(REASON)
           1731630   gpu_p13   pseudo  ssos938 CG       3:23      1 r6i5n2

 Done!
In [15]:
jobid = ['1731630']

TODO : vérifier qu'une trace a bien été générée dans le répertoire profiler/<name>_<jobid>_bs512_is176/ sous la forme d'un fichier .json:

In [16]:
!ls profiler/{name}_{jobid[0]}*
r6i5n2_1190187.1677501203007.pt.trace.json

Visualisation des traces profiler avec TensorBoard ¶

TODO : visualiser cette trace grâce à l'application TensorBoard en suivant les étapes suivantes :

  • ouvrir jupyterhub.idris.fr dans un nouvel onglet du navigateur
  • ouvrir une nouvelle instance JupyterHub en cliquant sur *Add New JupyterLab Instance*
  • demander la configuration suivante avec réservation d'un GPU :
  • charger le module pytorch-gpu/py3/1.11.0 via l'onglet Softwares (icône bleue sur le menu de gauche)

image.png

  • ouvrir l'application Tensorboard en cliquant sur l'icône correspondante

tensorboard.png

  • dans la fenêtre pop-up, définir l'argument CLI --logdir $WORK/DLO-JZ/profiler et cliquer sur Launch

Remarque : le premier démarrage de TensorBoard peut prendre un peu de temps. Il faut parfois faire preuve d'un peu de patience lorsqu'on utilise cet outil mais ça en vaut la peine :)

TODO : en naviguant dans les différents onglets du TensorBoard, chercher à répondre aux questions suivantes :

  • le GPU est-il bien utilisé ? (mémoire max utilisée, occupancy, efficiency)
  • la mémoire CPU est-elle saturée ?
  • les TensorCores sont-ils bien sollicités grâce à l'implémentation de la mixed precision ?
  • quelle partie de l'entraînement est la plus gourmande en temps ? se déroule-t-elle sur le CPU ou le GPU ?
  • essayer de repérer les grandes étapes de calcul sur la timeline de l'exécution (onglet Trace)

IMPORTANT : une fois le TP terminé, penser à quitter l'instance JupyterHub pour libérer le GPU ( > Hub Control Panel > Cancel ).

Garage

TP2_1 : Optimisation du DataLoader¶

Contrôle technique (version sous-optimisée)¶

TODO : lancer l'exécution sur 50 itérations (--test-nsteps 50) sans profiling pour passer un contrôle technique qui servira de référence. Cette exécution va prendre quelques minutes, vous pouvez passer à la suite du TP sans attendre la fin de l'exécution.

Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.

Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.

In [17]:
command = f'dlojz.py -b {bs_optim} --image-size {image_size} --test --test-nsteps 50'
command += f' --num-workers 0 --no-persistent-workers --no-pin-memory --no-non-blocking --prefetch-factor 2'
n_gpu = 1
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
                    account=account, time_max='00:10:00', constraint='v100-32g')
print(f'jobid = {jobid}')
batch job 0: 1 GPUs distributed on 1 nodes with 1 tasks / 1 gpus per node and 10 cpus per task
Submitted batch job 1732058
jobid = ['1732058']
In [18]:
display_slurm_queue(name)
             JOBID PARTITION     NAME     USER ST       TIME  NODES NODELIST(REASON)
           1732058   gpu_p13   pseudo  ssos938  R       4:26      1 r7i7n7

 Done!
In [19]:
jobid = ['1732058']
In [20]:
controle_technique(jobid)
Train throughput: 140.17 images/second
GPU throughput: 1774.97 images/second
epoch time: 9142.52 seconds
training time estimation for 90 epochs (with validations): 240.22 hours
-----------
training step time average (fwd/bkwd on GPU): 0.288455 sec (39.0%/60.7%) +/- 0.003221
loading step time average (CPU to GPU): 3.364171 sec +/- 0.120836
-----------
ELIGIBLE to run 6 epochs

Découverte de turbo_profiler¶

Pour ce TP, nous avons implémenté un profiler maison léger turbo_profiler basé sur l'outil Chronometer pour visualiser le temps passé sur CPU (DataLoader) et sur GPU (le reste de l'itération). Ce profiler est moins précis mais cela nous permettra de désactiver le profiler PyTorch pour ne pas dégrader les performances et éviter de devoir ouvrir l'outil graphique TensorBoard à chaque fois pour visualiser les informations qui nous intéressent.

TODO : relancer l'exécution précédente en désactivant le profiler PyTorch (sans l'argument --prof) et découvrir le profiler turbo_profiler.

Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.

Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.

In [21]:
command = f'dlojz.py -b {bs_optim} --image-size {image_size} --test --test-nsteps 15'
command += f' --num-workers 0 --no-persistent-workers --no-pin-memory --no-non-blocking --prefetch-factor 2'
n_gpu = 1
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
                    account=account, time_max='00:10:00', constraint='v100-32g')
print(f'jobid = {jobid}')
batch job 0: 1 GPUs distributed on 1 nodes with 1 tasks / 1 gpus per node and 10 cpus per task
Submitted batch job 1732254
jobid = ['1732254']

Puis, rebasculer la cellule précédente en mode Raw NBConvert, afin d'eviter de relancer un job par erreur.

In [22]:
display_slurm_queue(name)
             JOBID PARTITION     NAME     USER ST       TIME  NODES NODELIST(REASON)
           1732254   gpu_p13   pseudo  ssos938  R       3:07      1 r6i5n5

 Done!
In [23]:
jobid = ['1732254']

TODO : visualiser la sortie de turbo_profiler

In [24]:
turbo_profiler(jobid)
>>> Turbo Profiler >>> Training complete in 109.639733 s

Exploration des paramètres d'optimisation du DataLoader¶

L'objectif de ce TP est de réduire le temps passé sur CPU par le DataLoader.

Les différentes optimisations proposées par le DataLoader de PyTorch sont accessibles dans le script dlojz.py via les arguments :

  • --num-workers <num_workers> (défaut à 10)
  • --persistent-workers (défaut) ou --no-persistent-workers
  • --pin-memory (défaut) ou --no-pin-memory
  • --non-blocking (défaut) ou --no-non-blocking
  • --prefetch-factor <prefetch_factor> (défaut à 3)
  • --drop-last ou --no-drop-last (défaut)

TODO : faire varier ces différents paramètres et observer leurs effets grâce au profiler turbo_profiler

Remarque : pour cette étude, on ne lance les exécutions que sur 15 itérations (--test-nsteps 15) pour avancer plus rapidement.

Les différents essais seront stockés dans une DataFrame dataloader_trials :

In [88]:
import pandas as pd
dataloader_trials = pd.DataFrame({"jobid":pd.Series([],dtype=str),
                                  "num_workers":pd.Series([],dtype=int),
                                  "persistent_workers":pd.Series([],dtype=str),
                                  "pin_memory":pd.Series([],dtype=str),
                                  "non_blocking":pd.Series([],dtype=str),
                                  "prefetch_factor":pd.Series([],dtype=int),
                                  "drop_last":pd.Series([],dtype=str),
                                  "loading_time":pd.Series([],dtype=float),
                                  "training_time":pd.Series([],dtype=float)})

Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.

Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.

In [87]:
command = f'dlojz.py -b {bs_optim} --image-size {image_size} --test --test-nsteps 15'

# paramètres d'entrée correspondant aux optimisations du DataLoader
command += ' --num-workers 8' 
command += ' --persistent-workers'
command += ' --pin-memory'
command += ' --non-blocking'
command += ' --prefetch-factor 4'
command += ' --drop-last'

n_gpu = 1
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
                    account=account, time_max='00:10:00', constraint='v100-32g')
print(f'jobid = {jobid}')
batch job 0: 1 GPUs distributed on 1 nodes with 1 tasks / 1 gpus per node and 10 cpus per task
Submitted batch job 1732907
jobid = ['1732907']
In [89]:
display_slurm_queue(name)
             JOBID PARTITION     NAME     USER ST       TIME  NODES NODELIST(REASON)
           1732907   gpu_p13   pseudo  ssos938 CG       0:58      1 r9i6n6

 Done!
In [106]:
jobid = ['1732502']
jobid = ['1732530']
jobid = ['1732535']
jobid = ['1732560']
jobid = ['1732601']
jobid = ['1732602']
jobid = ['1732901']
jobid = ['1732903']
jobid = ['1732907']
In [107]:
# call turbo_profiler
dataloader_trial = turbo_profiler(jobid,dataloader_info=True)
# store result in "dataloader_trials" DataFrame
dataloader_trials = pd.concat([dataloader_trials,dataloader_trial], ignore_index=True)
>>> Turbo Profiler >>> Training complete in 24.194285 s
In [108]:
# afficher le tableau récapitulatif, trier par ordre croissant du LOADING_TIME
dataloader_trials.sort_values("loading_time")
Out[108]:
jobid num_workers persistent_workers pin_memory non_blocking prefetch_factor drop_last loading_time training_time
7 1732903 8 True True True 3 True 0.000372 26.406654
6 1732901 8 True True True 2 True 0.007613 24.314575
8 1732907 8 True True True 4 True 0.009204 24.194285
4 1732601 8 False False False 2 False 0.086541 21.986682
3 1732560 6 False False False 2 False 0.088905 23.359662
5 1732602 10 False False False 2 False 0.089680 30.414216
2 1732535 4 False False False 2 False 0.425653 33.483279
1 1732530 2 False False False 2 False 0.939929 46.934004
0 1732502 0 False False False 2 False 2.455895 84.524909
In [109]:
# afficher le tableau récapitulatif, trier par ordre croissant du TRAINING_TIME
dataloader_trials.sort_values("training_time")
Out[109]:
jobid num_workers persistent_workers pin_memory non_blocking prefetch_factor drop_last loading_time training_time
4 1732601 8 False False False 2 False 0.086541 21.986682
3 1732560 6 False False False 2 False 0.088905 23.359662
8 1732907 8 True True True 4 True 0.009204 24.194285
6 1732901 8 True True True 2 True 0.007613 24.314575
7 1732903 8 True True True 3 True 0.000372 26.406654
5 1732602 10 False False False 2 False 0.089680 30.414216
2 1732535 4 False False False 2 False 0.425653 33.483279
1 1732530 2 False False False 2 False 0.939929 46.934004
0 1732502 0 False False False 2 False 2.455895 84.524909

Visualisation des traces profiler avec TensorBoard (version optimisée)¶

TODO : après avoir choisi un lot de paramètres optimal, relancer le job en réactivant le profiler PyTorch (argument d'entrée --prof) afin de visualiser les traces sous TensorBoard, et les comparer avec la version non optimale étudiée dans le TP2_0.

Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.

Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.

In [111]:
command = f'dlojz.py -b {bs_optim} --image-size {image_size} --test --prof --test-nsteps 15'

# définir ici les paramètres optimaux 
command += ' --num-workers 8' 
command += ' --persistent-workers'
command += ' --pin-memory'
command += ' --non-blocking'
command += ' --prefetch-factor 3'
command += ' --drop-last'

n_gpu = 1
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
                    account=account, time_max='00:10:00', constraint='v100-32g')
print(f'jobid = {jobid}')
batch job 0: 1 GPUs distributed on 1 nodes with 1 tasks / 1 gpus per node and 10 cpus per task
Submitted batch job 1733126
jobid = ['1733126']

Puis, rebasculer la cellule précédente en mode Raw NBConvert, afin d'éviter de relancer un job par erreur.

In [112]:
display_slurm_queue(name)
             JOBID PARTITION     NAME     USER ST       TIME  NODES NODELIST(REASON)
           1733126   gpu_p13   pseudo  ssos938  R       1:20      1 r6i6n3

 Done!
In [113]:
jobid = ['1733126']

TODO : vérifier qu'une trace a bien été générée dans le répertoire profiler/<name>_<jobid>_bs512_is176/ sous la forme d'un fichier .json:

In [114]:
!ls profiler/{name}_{jobid[0]}*
r6i6n3_3655884.1677503618396.pt.trace.json

TODO : visualiser cette trace grâce à l'application TensorBoard (retrouver la procédure).

IMPORTANT : une fois le TP terminé, penser à quitter l'instance JupyterHub pour libérer le GPU ( > Hub Control Panel > Cancel ).

Contrôle technique (version optimisée)¶

TODO : lancer l'exécution sur 50 itérations (--test-nsteps 50) sans profiling pour passer un nouveau contrôle technique, à comparer avec celui de référence.

Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.

Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.

In [115]:
command = f'dlojz.py -b {bs_optim} --image-size {image_size} --test --test-nsteps 50'

# définir ici les paramètres optimaux 
command += ' --num-workers 8' 
command += ' --persistent-workers'
command += ' --pin-memory'
command += ' --non-blocking'
command += ' --prefetch-factor 3'
command += ' --drop-last'

n_gpu = 1
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
                    account=account, time_max='00:10:00', constraint='v100-32g')
print(f'jobid = {jobid}')
batch job 0: 1 GPUs distributed on 1 nodes with 1 tasks / 1 gpus per node and 10 cpus per task
Submitted batch job 1733293
jobid = ['1733293']
In [116]:
display_slurm_queue(name)
             JOBID PARTITION     NAME     USER ST       TIME  NODES NODELIST(REASON)
           1733293   gpu_p13   pseudo  ssos938  R       1:53      1 r7i7n5

 Done!
In [117]:
jobid = ['1733293']
In [118]:
controle_technique(jobid)
Train throughput: 1639.52 images/second
GPU throughput: 1657.92 images/second
epoch time: 781.34 seconds
training time estimation for 90 epochs (with validations): 22.55 hours
-----------
training step time average (fwd/bkwd on GPU): 0.308820 sec (44.2%/63.5%) +/- 0.062298
loading step time average (CPU to GPU): 0.003466 sec +/- 0.021396
-----------
ELIGIBLE to run 41 epochs

Garage

TP2_2 : Optimisation du DataLoader - Format WebDataset (Optionnel)¶

Le but de ce TP est d'utiliser un IterableDataset sur des données d'entrée au format WebDataset et de le comparer avec le Dataset Map-style de torchvision précédemment vu.

Implémentation du format WebDataset¶

TODO : dans le script dlojz.py :

  • Importer la librairie webdataset.
import webdataset as wds
  • Remplacer l'implémentation du train_dataset, du train_loader et du train_sampler par l'implémentation suivante.
train_dataset = (
        wds.WebDataset(os.environ['ALL_CCFRSCRATCH']+'/imagenet/webdataset/imagenet_train-{000000..000127}.tar', shardshuffle=True, nodesplitter=wds.split_by_node)
        .shuffle(1000)
        .decode("torchrgb")
        .to_tuple('input.pyd', 'output.pyd')
        .map_tuple(transform, lambda x: x)
        .batched(mini_batch_size)
        )

    dataset_size = 1281167
    number_of_batches = dataset_size // global_batch_size
    train_loader = wds.WebLoader(train_dataset,
                                 batch_size=None,
                                 num_workers=args.num_workers,
                                 persistent_workers=args.persistent_workers,
                                 pin_memory=args.pin_memory,
                                 prefetch_factor=args.prefetch_factor,
                                 drop_last=args.drop_last)

    train_loader = train_loader.slice(number_of_batches)
    train_loader.length = number_of_batches
  • Puisqu'il n'y a plus de train_sampler (la distribution des batches sur les différents workers se fait avec le paramètre nodesplitter=wds.split_by_node), effacer ou commenter la ligne suivante :
#train_sampler.set_epoch(epoch)
  • Un dataset de type IterableDataset ne connaissant pas sa longueur, la longueur du loader est définie par train_loader.length = number_of_batches. Modifier la déclaration de la variable N_batch en conséquence :
N_batch = train_loader.length

Contrôle technique (version sous-optimisée)¶

TODO : lancer l'exécution sur 50 itérations (--test-nsteps 50) sans profiling pour passer un contrôle technique qui servira de référence. Cette exécution va prendre quelques minutes, vous pouvez passer à la suite du TP sans attendre la fin de l'exécution.

Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.

Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.

In [14]:
command = f'dlojz.py -b {bs_optim} --image-size {image_size} --test --test-nsteps 50'
command += f' --num-workers 0 --no-persistent-workers --no-pin-memory --no-non-blocking --prefetch-factor 2'
n_gpu = 1
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
                    account=account, time_max='00:10:00', constraint='v100-32g')
print(f'jobid = {jobid}')
batch job 0: 1 GPUs distributed on 1 nodes with 1 tasks / 1 gpus per node and 10 cpus per task
Submitted batch job 1752495
jobid = ['1752495']
In [15]:
display_slurm_queue(name)
             JOBID PARTITION     NAME     USER ST       TIME  NODES NODELIST(REASON)
           1752495   gpu_p13   pseudo  cfor032  R       3:14      1 r7i4n2

 Done!
In [76]:
jobid = ['1752495']
In [17]:
controle_technique(jobid)
Train throughput: 264.22 images/second
GPU throughput: 1778.76 images/second
epoch time: 4848.33 seconds
training time estimation for 90 epochs (with validations): 135.39 hours
-----------
training step time average (fwd/bkwd on GPU): 0.287841 sec (39.0%/60.6%) +/- 0.001848
loading step time average (CPU to GPU): 1.649939 sec +/- 0.095673
-----------
ELIGIBLE to run 10 epochs
In [77]:
turbo_profiler(jobid)
>>> Turbo Profiler >>> Training complete in 159.014729 s

Visualisation des traces profiler Tensorboard (version sous-optimisée)¶

TODO : étudier les traces du cas sous-optimisé "num_workers=0" afin de mesurer l'accélération brute de ce type de Dataset.

Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.

Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.

In [18]:
command = f'dlojz.py -b {bs_optim} --image-size {image_size} --test --test-nsteps 15 --prof'
command += f' --num-workers 0 --no-persistent-workers --no-pin-memory --no-non-blocking --prefetch-factor 2'
n_gpu = 1
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
                    account=account, time_max='00:10:00', constraint='v100-32g')
print(f'jobid = {jobid}')
batch job 0: 1 GPUs distributed on 1 nodes with 1 tasks / 1 gpus per node and 10 cpus per task
Submitted batch job 1752570
jobid = ['1752570']

Puis, rebasculer la cellule précédente en mode Raw NBConvert, afin d'eviter de relancer un job par erreur.

In [19]:
display_slurm_queue(name)
             JOBID PARTITION     NAME     USER ST       TIME  NODES NODELIST(REASON)
           1752570   gpu_p13   pseudo  cfor032  R       2:39      1 r6i7n5

 Done!
In [20]:
#jobid = ['1587676']

TODO : vérifier qu'une trace a bien été générée dans le répertoire profiler/<name>_<jobid>_bs512_is176/ sous la forme d'un fichier .json:

In [21]:
!ls profiler/{name}_{jobid[0]}*
r6i7n5_864137.1677548609681.pt.trace.json

TODO : visualiser cette trace grâce à l'application TensorBoard (retrouver la procédure) et comparer les traces obtenues avec le dataset torchvision et le dataset webdataset.

IMPORTANT : une fois le TP terminé, penser à quitter l'instance JupyterHub pour libérer le GPU ( > Hub Control Panel > Cancel ).

Exploration des paramètres d'optimisation du DataLoader¶

Ensuite, l'objectif de ce TP est de réduire le temps passé sur CPU par le DataLoader WebDataset.

Les différentes optimisations proposées par le DataLoader sont accessibles dans le script dlojz.py via les arguments :

  • --num-workers <num_workers> (défaut à 10)
  • --persistent-workers (défaut) ou --no-persistent-workers
  • --pin-memory (défaut) ou --no-pin-memory
  • --non-blocking (défaut) ou --no-non-blocking
  • --prefetch-factor <prefetch_factor> (défaut à 3)
  • --drop-last ou --no-drop-last (défaut)

TODO : faire varier ces différents paramètres et observer leurs effets grâce au profiler turbo_profiler

Remarque : pour cette étude, on ne lance les exécutions que sur 15 itérations (--test-nsteps 15) pour avancer plus rapidement.

Les différents essais seront stockés dans une DataFrame dataloader_trials :

In [22]:
import pandas as pd
dataloader_trials = pd.DataFrame({"jobid":pd.Series([],dtype=str),
                                  "num_workers":pd.Series([],dtype=int),
                                  "persistent_workers":pd.Series([],dtype=str),
                                  "pin_memory":pd.Series([],dtype=str),
                                  "non_blocking":pd.Series([],dtype=str),
                                  "prefetch_factor":pd.Series([],dtype=int),
                                  "drop_last":pd.Series([],dtype=str),
                                  "loading_time":pd.Series([],dtype=float),
                                  "training_time":pd.Series([],dtype=float)})

Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.

Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.

In [59]:
command = f'dlojz.py -b {bs_optim} --image-size {image_size} --test --test-nsteps 15'

# paramètres d'entrée correspondant aux optimisations du DataLoader
command += ' --num-workers 8' 
command += ' --persistent-workers'
command += ' --pin-memory'
command += ' --non-blocking'
command += ' --prefetch-factor 4'
command += ' --drop-last'

n_gpu = 1
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
                    account=account, time_max='00:10:00', constraint='v100-32g')
print(f'jobid = {jobid}')
batch job 0: 1 GPUs distributed on 1 nodes with 1 tasks / 1 gpus per node and 10 cpus per task
Submitted batch job 1753252
jobid = ['1753252']
In [60]:
display_slurm_queue(name)
             JOBID PARTITION     NAME     USER ST       TIME  NODES NODELIST(REASON)
           1753252   gpu_p13   pseudo  cfor032  R       0:37      1 r7i4n2

 Done!
In [61]:
#jobid = ['1587801']
In [62]:
# call turbo_profiler
dataloader_trial = turbo_profiler(jobid,dataloader_info=True)
# store result in "dataloader_trials" DataFrame
dataloader_trials = pd.concat([dataloader_trials,dataloader_trial], ignore_index=True)
>>> Turbo Profiler >>> Training complete in 21.036211 s
In [63]:
# afficher le tableau récapitulatif, trier par ordre croissant du LOADING_TIME
dataloader_trials.sort_values("loading_time")
Out[63]:
jobid num_workers persistent_workers pin_memory non_blocking prefetch_factor drop_last loading_time training_time forward_backward_time iteration_time
4 1753098 8 True True True 2 True 0.000252 21.985507 0.299964 0.300216
6 1753252 8 True True True 4 True 0.000287 21.036211 0.369796 0.370083
5 1753169 8 True True True 3 True 0.007537 22.290722 0.300420 0.307957
0 1752650 4 False False False 2 False 0.074553 28.365277 0.287284 0.361837
2 1752953 8 False False False 2 False 0.091441 25.328325 0.286882 0.378323
1 1752833 6 False False False 2 False 0.094491 28.084319 0.289293 0.383784
3 1753053 10 False False False 2 False 0.099418 25.494731 0.287913 0.387331
In [64]:
# afficher le tableau récapitulatif, trier par ordre croissant du TRAINING_TIME
dataloader_trials.sort_values("training_time")
Out[64]:
jobid num_workers persistent_workers pin_memory non_blocking prefetch_factor drop_last loading_time training_time forward_backward_time iteration_time
6 1753252 8 True True True 4 True 0.000287 21.036211 0.369796 0.370083
4 1753098 8 True True True 2 True 0.000252 21.985507 0.299964 0.300216
5 1753169 8 True True True 3 True 0.007537 22.290722 0.300420 0.307957
2 1752953 8 False False False 2 False 0.091441 25.328325 0.286882 0.378323
3 1753053 10 False False False 2 False 0.099418 25.494731 0.287913 0.387331
1 1752833 6 False False False 2 False 0.094491 28.084319 0.289293 0.383784
0 1752650 4 False False False 2 False 0.074553 28.365277 0.287284 0.361837

Visualisation des traces profiler avec TensorBoard (version optimisée)¶

TODO : après avoir choisi un lot de paramètres optimal, relancer le job en réactivant le profiler PyTorch (argument d'entrée --prof) afin de visualiser les traces sous TensorBoard.

Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.

Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.

In [65]:
command = f'dlojz.py -b {bs_optim} --image-size {image_size} --test --prof --test-nsteps 15'

# définir ici les paramètres optimaux 
command += ' --num-workers 8' 
command += ' --persistent-workers'
command += ' --pin-memory'
command += ' --non-blocking'
command += ' --prefetch-factor 4'
command += ' --drop-last'

n_gpu = 1
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
                    account=account, time_max='00:10:00', constraint='v100-32g')
print(f'jobid = {jobid}')
batch job 0: 1 GPUs distributed on 1 nodes with 1 tasks / 1 gpus per node and 10 cpus per task
Submitted batch job 1753288
jobid = ['1753288']

Puis, rebasculer la cellule précédente en mode Raw NBConvert, afin d'éviter de relancer un job par erreur.

In [66]:
display_slurm_queue(name)
             JOBID PARTITION     NAME     USER ST       TIME  NODES NODELIST(REASON)
           1753288   gpu_p13   pseudo  cfor032  R       1:09      1 r6i7n8

 Done!

TODO : vérifier qu'une trace a bien été générée dans le répertoire profiler/<name>_<jobid>_bs512_is176/ sous la forme d'un fichier .json:

In [67]:
!ls profiler/{name}_{jobid[0]}*
r6i7n8_1573436.1677550134747.pt.trace.json

TODO : visualiser cette trace grâce à l'application TensorBoard (retrouver la procédure).

IMPORTANT : une fois le TP terminé, penser à quitter l'instance JupyterHub pour libérer le GPU ( > Hub Control Panel > Cancel ).

Contrôle technique (version optimisée)¶

TODO : lancer l'exécution sur 50 itérations (--test-nsteps 50) sans profiling pour passer un nouveau contrôle technique, à comparer avec celui de référence.

Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.

Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.

In [68]:
command = f'dlojz.py -b {bs_optim} --image-size {image_size} --test --test-nsteps 50'

# définir ici les paramètres optimaux 
command += ' --num-workers 8' 
command += ' --persistent-workers'
command += ' --pin-memory'
command += ' --non-blocking'
command += ' --prefetch-factor 4'
command += ' --drop-last'

n_gpu = 1
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
                    account=account, time_max='00:10:00', constraint='v100-32g')
print(f'jobid = {jobid}')
batch job 0: 1 GPUs distributed on 1 nodes with 1 tasks / 1 gpus per node and 10 cpus per task
Submitted batch job 1753334
jobid = ['1753334']
In [69]:
display_slurm_queue(name)
             JOBID PARTITION     NAME     USER ST       TIME  NODES NODELIST(REASON)
           1753334   gpu_p13   pseudo  cfor032  R       0:44      1 r6i7n8

 Done!
In [70]:
#jobid = ['1587014']
In [71]:
controle_technique(jobid)
Train throughput: 1682.85 images/second
GPU throughput: 1684.37 images/second
epoch time: 761.22 seconds
training time estimation for 90 epochs (with validations): 20.83 hours
-----------
training step time average (fwd/bkwd on GPU): 0.303971 sec (43.5%/66.5%) +/- 0.081592
loading step time average (CPU to GPU): 0.000274 sec +/- 0.000051
-----------
ELIGIBLE to run 41 epochs

Garage

TP2_3 : Data Augmentation (Optionnel)¶

TP2_3_1 : RandAugment¶

Le but de ce TP est d'ajouter la transformation RandAugment (disponible dans torchvision) dans la liste des transformations pour la Data Augmentation et de mesurer grâce au profiler ce que cela implique pour le DataLoader.

Il faut repartir d'un scriptdlojz.py propre :

In [72]:
# copier/coller la solution si nécessaire
!cp solutions/dlojz2_0.py dlojz.py
In [73]:
import os
import torchvision
import torchvision.transforms as transforms
import torchvision.models as models
import torch
import numpy as np
import matplotlib.pyplot as plt

transform = transforms.Compose([ 
        transforms.RandomResizedCrop(176),  # Random resize - Data Augmentation
        transforms.RandomHorizontalFlip(),  # Horizontal Flip - Data Augmentation
        transforms.RandAugment(5, 9),       # Random Augmentation 2: n operations, 9 : magnitude 
        transforms.ToTensor()               # convert the PIL Image to a tensor
        ])
    
    
train_dataset = torchvision.datasets.ImageNet(root=os.environ['ALL_CCFRSCRATCH']+'/imagenet',
                                                  transform=transform)
train_dataset
Out[73]:
Dataset ImageNet
    Number of datapoints: 1281167
    Root location: /gpfsscratch/idris/for/commun/imagenet
    Split: train
    StandardTransform
Transform: Compose(
               RandomResizedCrop(size=(176, 176), scale=(0.08, 1.0), ratio=(0.75, 1.3333), interpolation=bilinear)
               RandomHorizontalFlip(p=0.5)
               RandAugment(num_ops=5, magnitude=9, num_magnitude_bins=31, interpolation=InterpolationMode.NEAREST, fill=None)
               ToTensor()
           )
In [74]:
%%time

train_loader = torch.utils.data.DataLoader(dataset=train_dataset,    
                                           batch_size=4,
                                           shuffle=True)
batch = next(iter(train_loader))
print('X train batch, shape: {}, data type: {}, Memory usage: {} bytes'
      .format(batch[0].shape, batch[0].dtype, batch[0].element_size()*batch[0].nelement()))
print('Y train batch, shape: {}, data type: {}, Memory usage: {} bytes'
      .format(batch[1].shape, batch[1].dtype, batch[1].element_size()*batch[1].nelement()))

for i in range(4):
    img = batch[0][i].numpy().transpose((1,2,0))
    plt.imshow(img)
    plt.axis('off')
    plt.show()
X train batch, shape: torch.Size([4, 3, 176, 176]), data type: torch.float32, Memory usage: 1486848 bytes
Y train batch, shape: torch.Size([4]), data type: torch.int64, Memory usage: 32 bytes
CPU times: user 2.06 s, sys: 350 ms, total: 2.41 s
Wall time: 2.61 s

TODO : dans le script dlojz.py :

  • Rajouter la transformation RandAugmentdans la liste des transformations pour la Data Augmentation
transform = transforms.Compose([ 
        transforms.RandomResizedCrop(args.image_size),  # Random resize - Data Augmentation
        transforms.RandomHorizontalFlip(),              # Horizontal Flip - Data Augmentation
        transforms.RandAugment(2, 9),                   # Random Augmentation 2:n operations, 9:magnitude 
        transforms.ToTensor(),                          # convert the PIL Image to a tensor
        transforms.Normalize(mean=(0.485, 0.456, 0.406),
                             std=(0.229, 0.224, 0.225))
        ])
In [75]:
command = f'dlojz.py -b {bs_optim} --image-size {image_size} --test'
command
Out[75]:
'dlojz.py -b 512 --image-size 176 --test'

Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.

Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.

In [79]:
command = f'dlojz.py -b {bs_optim} --image-size {image_size} --test'
n_gpu = 1
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
                    account=account, time_max='00:10:00', constraint='v100-32g')
print(f'jobid = {jobid}')
batch job 0: 1 GPUs distributed on 1 nodes with 1 tasks / 1 gpus per node and 10 cpus per task
Submitted batch job 1753749
jobid = ['1753749']

Copier-coller la sortie jobid = ['xxxxx'] dans la cellule suivante.

Puis, rebasculer la cellule précédente en mode Raw NBConvert, afin d'éviter de relancer un job par erreur.

In [89]:
jobid = ['1753749']
In [90]:
display_slurm_queue(name)
 Done!
In [91]:
controle_technique(jobid)
Train throughput: 1581.79 images/second
GPU throughput: 1691.21 images/second
epoch time: 810.18 seconds
training time estimation for 90 epochs (with validations): 23.43 hours
-----------
training step time average (fwd/bkwd on GPU): 0.302742 sec (43.0%/64.0%) +/- 0.070215
loading step time average (CPU to GPU): 0.020941 sec +/- 0.141758
-----------
ELIGIBLE to run 40 epochs
In [92]:
turbo_profiler(jobid)
>>> Turbo Profiler >>> Training complete in 40.086319 s

Commentaires


TP2_3_2 : Mixup¶

Le but de ce TP est d'ajouter la transformation Mixup dans la liste des transformations pour la Data Augmentation et de mesurer grâce au profiler ce que cela implique pour le DataLoader.

La transformation MixUp n'est pas disponible dans torchvision, le script est disponible dans le répertoire mixup/. On notera que cette transformation impacte à la fois l'image et le label.

On choisira, comme cela est fait habituellement, de mixer 2 images présentes dans le batch généré par le DataLoader. Donc cette transformation sera faite dans la boucle d'apprentissage après génération du batch et après toutes autres transformations liées à la Data Augmentation.

In [93]:
import os
import torchvision
import torchvision.transforms as transforms
import torchvision.models as models
import torch
import numpy as np
import matplotlib.pyplot as plt

transform = transforms.Compose([ 
        transforms.RandomResizedCrop(176),  # Random resize - Data Augmentation
        transforms.RandomHorizontalFlip(),  # Horizontal Flip - Data Augmentation
        transforms.ToTensor()               # convert the PIL Image to a tensor
        ])
    
    
train_dataset = torchvision.datasets.ImageNet(root=os.environ['ALL_CCFRSCRATCH']+'/imagenet',
                                                  transform=transform)
train_dataset
Out[93]:
Dataset ImageNet
    Number of datapoints: 1281167
    Root location: /gpfsscratch/idris/for/commun/imagenet
    Split: train
    StandardTransform
Transform: Compose(
               RandomResizedCrop(size=(176, 176), scale=(0.08, 1.0), ratio=(0.75, 1.3333), interpolation=bilinear)
               RandomHorizontalFlip(p=0.5)
               ToTensor()
           )
In [94]:
from mixup.mixup import mixup_data
In [95]:
%%time

train_loader = torch.utils.data.DataLoader(dataset=train_dataset,    
                                           batch_size=16,
                                           shuffle=True)
batch = next(iter(train_loader))
print('X train batch, shape: {}, data type: {}, Memory usage: {} bytes'
      .format(batch[0].shape, batch[0].dtype, batch[0].element_size()*batch[0].nelement()))
print('Y train batch, shape: {}, data type: {}, Memory usage: {} bytes'
      .format(batch[1].shape, batch[1].dtype, batch[1].element_size()*batch[1].nelement()))

imgs, targets = batch
imgs, targets = mixup_data(imgs, targets, num_classes=1000, alpha=2)        ## Transformation Mixup


for i in range(4):
    img = imgs[i].numpy().transpose((1,2,0))
    plt.imshow(img)
    plt.axis('off')
    plt.show()
    print(f'target : {torch.max(targets, dim=1)[1][i]}, lambda : {torch.max(targets, dim=1)[0][i]}')
X train batch, shape: torch.Size([16, 3, 176, 176]), data type: torch.float32, Memory usage: 5947392 bytes
Y train batch, shape: torch.Size([16]), data type: torch.int64, Memory usage: 128 bytes
target : 295, lambda : 0.6783640384674072
target : 660, lambda : 0.5441898703575134
target : 69, lambda : 0.6190118789672852
target : 750, lambda : 0.6442692279815674
CPU times: user 5.8 s, sys: 116 ms, total: 5.92 s
Wall time: 6.2 s

Paramètre alpha pour la beta distribution

Dans le script mixup.py, la variable lambda (lam) correspond à la proportion de la première image par rapport à la deuxième image. Elle est choisie aléatoirement suivant une distribution bêta définie sur [0, 1].

Le paramètre alpha agit sur la forme de la distribution bêta. alpha = 1 correspond à une distribution uniforme, alpha < 1 favorise un tirage au sort de valeurs proches des bornes 0. ou 1., et alpha > 1 favorise un tirage au sort de valeurs proches du centre 0.5.

In [96]:
for alpha in [0.5, 1., 2.]:
    plt.hist(np.random.beta(alpha, alpha, 1000000), bins=50, density=True, histtype='step')
    plt.title(f'alpha={alpha}')
    plt.show()

Transformation Mixup sur CPU¶

TODO : dans le script dlojz.py :

  • Importer la transformation Mixup
from mixup.mixup import mixup_data
  • Rajouter la transformation MixUp dans la boucle d'apprentissage avant d'envoyer le batch d'images et de labels au GPU.
# distribution of images and labels to all GPUs                                
    images, labels = mixup_data(images, labels, num_classes=1000, alpha=2.)
    images = images.to(gpu, non_blocking=True)
    labels = labels.to(gpu, non_blocking=True)
  • Dans le calcul des métriques à la fin de la boucle d'apprentissage, étant donné que les labels ne sont plus des id de classes mais des vecteurs de type one hot encoded, il faut ajouter la ligne suivante pour calculer les valeurs maximales des vecteurs :
# Metric mesurement
    _, predicted = torch.max(outputs.data, 1)
    labels = torch.argmax(labels, dim=1)     ### line to add for Mixup and Cutmix
    accuracy = (predicted == labels).sum() / labels.size(0)
In [97]:
command = f'dlojz.py -b {bs_optim} --image-size {image_size} --test'
command
Out[97]:
'dlojz.py -b 512 --image-size 176 --test'

Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.

Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.

In [99]:
command = f'dlojz.py -b {bs_optim} --image-size {image_size} --test'
n_gpu = 1
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
                    account=account, time_max='00:10:00', constraint='v100-32g')
print(f'jobid = {jobid}')
batch job 0: 1 GPUs distributed on 1 nodes with 1 tasks / 1 gpus per node and 10 cpus per task
Submitted batch job 1754157
jobid = ['1754157']

Copier-coller la sortie jobid = ['xxxxx'] dans la cellule suivante.

Puis, rebasculer la cellule précédente en mode Raw NBConvert, afin d'éviter de relancer un job par erreur.

In [100]:
#jobid = ['1910208']
In [101]:
display_slurm_queue(name)
             JOBID PARTITION     NAME     USER ST       TIME  NODES NODELIST(REASON)
           1754157   gpu_p13   pseudo  cfor032  R       1:11      1 r7i4n2

 Done!
In [102]:
controle_technique(jobid)
Train throughput: 933.81 images/second
GPU throughput: 1797.20 images/second
epoch time: 1372.38 seconds
training time estimation for 90 epochs (with validations): 36.42 hours
-----------
training step time average (fwd/bkwd on GPU): 0.284888 sec (8.4%/93.9%) +/- 0.032209
loading step time average (CPU to GPU): 0.263405 sec +/- 0.056622
-----------
ELIGIBLE to run 29 epochs
In [103]:
turbo_profiler(jobid)
>>> Turbo Profiler >>> Training complete in 46.986467 s

Transformation Mixup sur GPU¶

TODO : dans le script dlojz.py :

  • Appliquer la transformation MixUp dans la boucle d'apprentissage après avoir envoyé le batch d'images et de labels au GPU.
# distribution of images and labels to all GPUs                                
    #images, labels = mixup_data(images, labels, num_classes=1000, alpha=2.) ## ligne déplacée
    images = images.to(gpu, non_blocking=args.non_blocking)
    labels = labels.to(gpu, non_blocking=args.non_blocking)
    images, labels = mixup_data(images, labels, num_classes=1000, alpha=2., device=gpu)

TODO : dans le script mixup/mixup.py :

  • Ajouter le paramètre device=device à chaque fois que l'on crée un nouveau Tensor pour qu'il soit stocké en mémoire au bon emplacement (CPU ou GPU).

Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.

Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.

In [104]:
command = f'dlojz.py -b {bs_optim} --image-size {image_size} --test'
n_gpu = 1
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
                    account=account, time_max='00:10:00', constraint='v100-32g')
print(f'jobid = {jobid}')
batch job 0: 1 GPUs distributed on 1 nodes with 1 tasks / 1 gpus per node and 10 cpus per task
Submitted batch job 1754228
jobid = ['1754228']

Copier-coller la sortie jobid = ['xxxxx'] dans la cellule suivante.

Puis, rebasculer la cellule précédente en mode Raw NBConvert, afin d'éviter de relancer un job par erreur.

In [105]:
#jobid = ['1910460']
In [106]:
display_slurm_queue(name)
             JOBID PARTITION     NAME     USER ST       TIME  NODES NODELIST(REASON)
           1754228   gpu_p13   pseudo  cfor032  R       1:48      1 r7i3n2

 Done!
In [107]:
controle_technique(jobid)
Train throughput: 1662.62 images/second
GPU throughput: 1777.90 images/second
epoch time: 770.79 seconds
training time estimation for 90 epochs (with validations): 22.15 hours
-----------
training step time average (fwd/bkwd on GPU): 0.287980 sec (8.2%/95.3%) +/- 0.036295
loading step time average (CPU to GPU): 0.019968 sec +/- 0.006211
-----------
ELIGIBLE to run 41 epochs
In [108]:
turbo_profiler(jobid)
>>> Turbo Profiler >>> Training complete in 39.727482 s

Commentaires


TP2_3_3 : Cutmix¶

Le but de ce TP est d'ajouter la transformation CutMix dans la liste des transformations pour la Data Augmentation et de mesurer grâce au profiler ce que cela implique pour le DataLoader.

La transformation CutMix n'est pas disponible dans torchvision, le script est disponible dans le répertoire cutmix/. On notera que cette transformation impacte à la fois l'image et le label.

On choisira, comme cela est fait habituellement, de mixer 2 images présentes dans le batch généré par le dataloader. Donc cette transformation sera faite dans la boucle d'apprentissage après génération du batch et donc après toutes autres transformations liées à la Data Augmentation.

Dans le script cutmix.py, la variable lambda (lam) correspond à la proportion de la première image par rapport à la deuxième image. Elle est choisie aléatoirement suivant une distribution uniforme définie sur [0, 1].

In [109]:
import os
import torchvision
import torchvision.transforms as transforms
import torchvision.models as models
import torch
import numpy as np
import matplotlib.pyplot as plt

transform = transforms.Compose([ 
        transforms.RandomResizedCrop(176),  # Random resize - Data Augmentation
        transforms.RandomHorizontalFlip(),  # Horizontal Flip - Data Augmentation
        transforms.ToTensor()               # convert the PIL Image to a tensor
        ])
    
    
train_dataset = torchvision.datasets.ImageNet(root=os.environ['ALL_CCFRSCRATCH']+'/imagenet',
                                                  transform=transform)
train_dataset
Out[109]:
Dataset ImageNet
    Number of datapoints: 1281167
    Root location: /gpfsscratch/idris/for/commun/imagenet
    Split: train
    StandardTransform
Transform: Compose(
               RandomResizedCrop(size=(176, 176), scale=(0.08, 1.0), ratio=(0.75, 1.3333), interpolation=bilinear)
               RandomHorizontalFlip(p=0.5)
               ToTensor()
           )
In [110]:
from cutmix.cutmix import cutmix_data
In [111]:
%%time

train_loader = torch.utils.data.DataLoader(dataset=train_dataset,    
                                           batch_size=16,
                                           shuffle=True)
batch = next(iter(train_loader))
print('X train batch, shape: {}, data type: {}, Memory usage: {} bytes'
      .format(batch[0].shape, batch[0].dtype, batch[0].element_size()*batch[0].nelement()))
print('Y train batch, shape: {}, data type: {}, Memory usage: {} bytes'
      .format(batch[1].shape, batch[1].dtype, batch[1].element_size()*batch[1].nelement()))

imgs, targets = batch
imgs, targets = cutmix_data(imgs, targets, num_classes=1000)


for i in range(4):
    img = imgs[i].numpy().transpose((1,2,0))
    plt.imshow(img)
    plt.axis('off')
    plt.show()
    print(f'target : {torch.max(targets, dim=1)[1][i]}, lambda : {torch.max(targets, dim=1)[0][i]}')
X train batch, shape: torch.Size([16, 3, 176, 176]), data type: torch.float32, Memory usage: 5947392 bytes
Y train batch, shape: torch.Size([16]), data type: torch.int64, Memory usage: 128 bytes
target : 259, lambda : 0.7369576692581177
target : 671, lambda : 0.7893530130386353
target : 832, lambda : 1.0
target : 545, lambda : 0.6022727489471436
CPU times: user 6.14 s, sys: 151 ms, total: 6.29 s
Wall time: 6.55 s

Transformation CutMix sur GPU¶

TODO : dans le script dlojz.py :

  • Importer la transformation CutMix
from cutmix.cutmix import cutmix_data
  • Rajouter la transformation CutMix dans la boucle d'apprentissage après avoir envoyé le batch d'images et de labels au GPU.
# distribution of images and labels to all GPUs
    images = images.to(gpu, non_blocking=args.non_blocking)
    labels = labels.to(gpu, non_blocking=args.non_blocking)
    images, labels = cutmix_data(images, labels, num_classes=1000, device=gpu)

Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.

Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.

In [112]:
command = f'dlojz.py -b {bs_optim} --image-size {image_size} --test'
n_gpu = 1
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
                    account=account, time_max='00:10:00', constraint='v100-32g')
print(f'jobid = {jobid}')
batch job 0: 1 GPUs distributed on 1 nodes with 1 tasks / 1 gpus per node and 10 cpus per task
Submitted batch job 1754333
jobid = ['1754333']

Copier-coller la sortie jobid = ['xxxxx'] dans la cellule suivante.

Puis, rebasculer la cellule précédente en mode Raw NBConvert, afin d'éviter de relancer un job par erreur.

In [113]:
#jobid = ['226430']
In [114]:
display_slurm_queue(name)
             JOBID PARTITION     NAME     USER ST       TIME  NODES NODELIST(REASON)
           1754333   gpu_p13   pseudo  cfor032  R       1:22      1 r7i6n8

 Done!
In [115]:
controle_technique(jobid)
Train throughput: 1318.64 images/second
GPU throughput: 1780.96 images/second
epoch time: 971.86 seconds
training time estimation for 90 epochs (with validations): 26.62 hours
-----------
training step time average (fwd/bkwd on GPU): 0.287486 sec (8.5%/93.8%) +/- 0.043484
loading step time average (CPU to GPU): 0.100793 sec +/- 0.037892
-----------
ELIGIBLE to run 36 epochs
In [116]:
turbo_profiler(jobid)
>>> Turbo Profiler >>> Training complete in 41.483737 s

Optimisation de la transformation CutMix¶

Le code précédent utilise une boucle for qui empêche de distribuer la transformation sur les cores du GPU. Chaque image dans le batch est traitée de manière séquentielle.

Le but de cette partie est d'optimiser le code de CutMix en générant du calcul matriciel adapté à une parallélisation sur GPU. Il s'agira de manipuler des tenseurs de tailles proportionnelles au batch size et d'utiliser des fonctions d'algèbre linéaire pour aboutir au même résultat numérique tout en accélérant le calcul.

En d'autres termes, au lieu de constituer un masque par image, nous allons directement créer un batch de masques pour tout un batch d'images.

Création d'un batch de masques

Dans un premier temps, pour comprendre la procédure, nous travaillerons avec un batch de 3 images de taille 32x32.

In [117]:
import torch
import numpy as np
import matplotlib.pyplot as plt
batch_size = 3
W = 32
H = 32

En entrée, on connait les coordonnées des coins du cadre à découper pour chaque image du batch (voir illustration ci-dessous).

In [118]:
# coordonnee min dans la largeur pour chaque image du batch
x1 = torch.Tensor([10, 5, 23]).long()
# coordonne max dans la largeur pour chaque image du batch
x2 =  torch.Tensor([20, 25, 31]).long()
# coordonnee min dans la hauteur pour chaque image du batch
y1 =  torch.Tensor([5, 10, 0]).long()
# coordonne max dans la hauteur pour chaque image du batch
y2 =  torch.Tensor([10, 22, 20]).long()

cutmix_opt

1. Création des vecteurs ligne "largeur" w_int et des vecteurs colonne "hauteur" h_int pour tout le batch d'images

In [119]:
# initialisation à zéro
w_int = torch.zeros(batch_size,1,W) # vecteurs ligne
h_int = torch.zeros(batch_size,H,1) # vecteurs colonne

On initialise les éléments correspondant aux coordonnées minimales (x1 et y1) à 1.
On initialise les éléments correspondant aux coordonnées maximales (x2 et y2) à -1.
Par la suite, les intervalles [x1,x2] et [y1,y2] seront remplis de 1 en demandant à remplir chaque vecteur avec la somme cumulée de ses éléments.

In [120]:
batch_idx = torch.arange(0,batch_size)
# initialisation des indices correspondant aux coord min x1 et y1 à 1
w_int[batch_idx,0,x1] = 1.
h_int[batch_idx,y1,0] = 1.

# initialisation des indices correspondant aux coord max x2 et y2 à -1
w_int[batch_idx,0,x2] = -1.
h_int[batch_idx,y2,0] = -1.
In [121]:
# visualisation des vecteurs ligne "largeur" w_int
for wx in w_int:
    plt.imshow(wx)
    plt.clim(-1,1)
    plt.colorbar(ticks=np.arange(-1,2))
    plt.show()
In [122]:
# visualisation des vecteurs colonne "hauteur"
for hx in h_int:
    plt.imshow(hx)
    plt.clim(-1,1)
    plt.colorbar(ticks=np.arange(-1,2))
    plt.show()

Pour créer nos vecteurs w_int et h_int, on remplit chaque intervalle [x1,x2] et [y1,y2] de 1 en utilisant la fonction torch.cumsum pour cumuler les valeurs des éléments des vecteurs.

In [123]:
# torch.cumsum(input, dim, *, dtype=None, out=None) → Tensor
# Returns the cumulative sum of elements of input in the dimension dim.
# Parameters
#        input (Tensor) – the input tensor.
#        dim (int) – the dimension to do the operation over

w_int = torch.cumsum(w_int, dim=2) # vecteurs ligne
h_int = torch.cumsum(h_int, dim=1) # vecteurs colonne
In [124]:
# visualisation des vecteurs masques "largeur"
for wx in w_int:
    plt.imshow(wx)
    plt.clim(-1,1)
    plt.colorbar(ticks=np.arange(-1,2))
    plt.show()
In [125]:
# visualisation des vecteurs masques "hauteur"
for hx in h_int:
    plt.imshow(hx)
    plt.clim(-1,1)
    plt.colorbar(ticks=np.arange(-1,2))
    plt.show()

2. Créations du batch de masques intérieurs et extérieurs

  • Multiplication des vecteurs h_int et w_int pour obtenir les masques intérieurs pour chaque image du batch.
In [126]:
# multiplication des vecteurs colonne "hauteur" h_int par les vecteurs ligne "largeur" w_int
mask_int = h_int*w_int
In [127]:
# visualisation des masques intérieurs pour chaque image du batch
for m in mask_int:
    plt.imshow(m)
    plt.clim(-1,1)
    plt.colorbar(ticks=np.arange(-1,2))
    plt.show()
  • Puis, création des masques extérieurs à partir des masques intérieurs.
In [128]:
# les masques extérieurs sont les complémentaires des masques intérieurs
mask_ext = mask_int * (-1) + 1
In [129]:
# visualisation des masques extérieurs
for m in mask_ext:
    plt.imshow(m)
    plt.clim(-1,1)
    plt.colorbar(ticks=np.arange(-1,2))
    plt.show()

Implémentation de la fonction de création d'un batch de masques

Maintenant, l'idée est d'implémenter ce qui a été fait dans les cellules précédentes dans une fontion générique, en ajoutant un choix sur le device d'exécution.

TODO : implémenter la fonction de création des masques dans la cellule suivante. Les entrées de la fonction sont :

  • les coordonnées x1, x2, y1, y2,
  • le batch_size,
  • la largeurW de l'image,
  • la hauteur H de l'image,
  • le device de calcul.

Important : Pour les images RGB (channel de 3), il faut rajouter une dimension en deuxième position dans les masques finaux :

# rajouter une dimension en 2e position pour pouvoir traiter des images RGB
    mask_int = mask_int.unsqueeze(1) 
    mask_ext = mask_ext.unsqueeze(1)

Attention : Ne pas oublier le paramètre device=device à chaque création d'un nouveau Tensor. Par exemple pour :

w_int = torch.zeros(batch_size,1,W,device=device)
In [130]:
def cut_mask(x1, x2, y1, y2, batch_size, W, H, device=None):
    
    mask_ext, mask_int = None, None
    
    ### TODO

    # initialisation à zéro
    w_int = torch.zeros(batch_size,1,W,device=device) # vecteurs ligne
    h_int = torch.zeros(batch_size,H,1,device=device) # vecteurs colonne
    
    batch_idx = torch.arange(0,batch_size,device=device)
    # initialisation des indices correspondant aux coord min x1 et y1 à 1
    w_int[batch_idx,0,x1] = 1.
    h_int[batch_idx,y1,0] = 1.

    # initialisation des indices correspondant aux coord max x2 et y2 à -1
    w_int[batch_idx,0,x2] = -1.
    h_int[batch_idx,y2,0] = -1.
    
    w_int = torch.cumsum(w_int, dim=2) # vecteurs ligne
    h_int = torch.cumsum(h_int, dim=1) # vecteurs colonne

    # multiplication des vecteurs colonne "hauteur" h_int par les vecteurs ligne "largeur" w_int
    mask_int = h_int*w_int
    
    # les masques extérieurs sont les complémentaires des masques intérieurs
    mask_ext = mask_int * (-1) + 1
    
    # rajouter une dimension pour les images RGB
    mask_int = mask_int.unsqueeze(1)
    mask_ext = mask_ext.unsqueeze(1) 
    
    return mask_ext, mask_int

Test de la fonction implémentée¶

In [131]:
%%time

train_loader = torch.utils.data.DataLoader(dataset=train_dataset,    
                                           batch_size=16,
                                           shuffle=True)
batch = next(iter(train_loader))
print('X train batch, shape: {}, data type: {}, Memory usage: {} bytes'
      .format(batch[0].shape, batch[0].dtype, batch[0].element_size()*batch[0].nelement()))
print('Y train batch, shape: {}, data type: {}, Memory usage: {} bytes'
      .format(batch[1].shape, batch[1].dtype, batch[1].element_size()*batch[1].nelement()))

imgs, targets = batch
X train batch, shape: torch.Size([16, 3, 176, 176]), data type: torch.float32, Memory usage: 5947392 bytes
Y train batch, shape: torch.Size([16]), data type: torch.int64, Memory usage: 128 bytes
CPU times: user 4.01 s, sys: 38.7 ms, total: 4.05 s
Wall time: 4.17 s
In [132]:
batch_size = 16
W = 176
H = 176
In [133]:
lam = torch.rand(batch_size)
s_index = torch.randperm(batch_size)      # Shuffle index
rand_x = torch.randint(W, (batch_size,))
rand_y = torch.randint(H, (batch_size,))
cut_rat = torch.sqrt(1. - lam) ## cut ratio according to the random lambda

x1 = torch.clip(rand_x - rand_x / 2, min=0).long()
x2 = torch.clip(rand_x + rand_x / 2, max=W-1).long()
y1 = torch.clip(rand_y - rand_y / 2, min=0).long()
y2 = torch.clip(rand_y + rand_y / 2, max=H-1).long()

mask_ext, mask_int = cut_mask(x1, x2, y1, y2, batch_size, W, H)
In [134]:
# vérifier si le masque et l'image ont le même nombre de dimensions
try:
    assert imgs.dim() == mask_int.dim()
    print('OK!')
except:
    print(f'Mismatch: \n dim imgs = {imgs.dim()} \n dim mask = {mask_int.dim()} ')
OK!
In [135]:
imgs = mask_ext * imgs + mask_int * imgs[s_index, :]
In [136]:
for i in range(4):
    img = imgs[i].numpy().transpose((1,2,0))
    plt.imshow(img)
    plt.axis('off')
    plt.show()

Puis si le résultat est satisfaisant, intégrer la fonction dans le code cutmix/cutmix.py.

TODO : dans le script cutmix/cutmix.py, ajouter la fonction cut_mask définie dans la cellule plus haut.

Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.

Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.

In [137]:
command = f'dlojz.py -b {bs_optim} --image-size {image_size} --test'
n_gpu = 1
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
                    account=account, time_max='00:10:00', constraint='v100-32g')
print(f'jobid = {jobid}')
batch job 0: 1 GPUs distributed on 1 nodes with 1 tasks / 1 gpus per node and 10 cpus per task
Submitted batch job 1754433
jobid = ['1754433']

Copier-coller la sortie jobid = ['xxxxx'] dans la cellule suivante.

Puis, rebasculer la cellule précédente en mode Raw NBConvert, afin d'éviter de relancer un job par erreur.

In [138]:
#jobid = ['256363']
In [139]:
display_slurm_queue(name)
             JOBID PARTITION     NAME     USER ST       TIME  NODES NODELIST(REASON)
           1754433   gpu_p13   pseudo  cfor032  R       1:28      1 r6i5n4

 Done!
In [140]:
controle_technique(jobid)
Train throughput: 1671.57 images/second
GPU throughput: 1791.99 images/second
epoch time: 766.66 seconds
training time estimation for 90 epochs (with validations): 22.23 hours
-----------
training step time average (fwd/bkwd on GPU): 0.285715 sec (8.7%/95.9%) +/- 0.038901
loading step time average (CPU to GPU): 0.020583 sec +/- 0.004476
-----------
ELIGIBLE to run 41 epochs
In [141]:
turbo_profiler(jobid)
>>> Turbo Profiler >>> Training complete in 40.940357 s
In [ ]: